package sbt.internal

import java.lang.{ Process => JProcess, ProcessBuilder => JProcessBuilder }
import java.io.{ Closeable, File, IOException }
import java.io.{ BufferedReader, InputStream, InputStreamReader, OutputStream, PipedInputStream, PipedOutputStream }
import java.net.URL

trait ProcessExtra {
  import Process._
  implicit def builderToProcess(builder: JProcessBuilder): ProcessBuilder = apply(builder)
  implicit def fileToProcess(file: File): FilePartialBuilder = apply(file)
  implicit def urlToProcess(url: URL): URLPartialBuilder = apply(url)

  implicit def buildersToProcess[T](builders: Seq[T])(implicit convert: T => SourcePartialBuilder): Seq[SourcePartialBuilder] = applySeq(builders)

  implicit def stringToProcess(command: String): ProcessBuilder = apply(command)
  implicit def stringSeqToProcess(command: Seq[String]): ProcessBuilder = apply(command)
}

/** Methods for constructing simple commands that can then be combined. */
object Process extends ProcessExtra {
  def apply(command: String): ProcessBuilder = apply(command, None)

  def apply(command: Seq[String]): ProcessBuilder = apply(command.toArray, None)

  def apply(command: String, arguments: Seq[String]): ProcessBuilder = apply(command :: arguments.toList, None)
  /** create ProcessBuilder with working dir set to File and extra environment variables */
  def apply(command: String, cwd: File, extraEnv: (String, String)*): ProcessBuilder =
    apply(command, Some(cwd), extraEnv: _*)
  /** create ProcessBuilder with working dir set to File and extra environment variables */
  def apply(command: Seq[String], cwd: File, extraEnv: (String, String)*): ProcessBuilder =
    apply(command, Some(cwd), extraEnv: _*)
  /** create ProcessBuilder with working dir optionally set to File and extra environment variables */
  def apply(command: String, cwd: Option[File], extraEnv: (String, String)*): ProcessBuilder = {
    apply(command.split("""\s+"""), cwd, extraEnv: _*)
    // not smart to use this on windows, because CommandParser uses \ to escape ".
    /*CommandParser.parse(command) match {
      case Left(errorMsg) => error(errorMsg)
      case Right((cmd, args)) => apply(cmd :: args, cwd, extraEnv : _*)
    }*/
  }
  /** create ProcessBuilder with working dir optionally set to File and extra environment variables */
  def apply(command: Seq[String], cwd: Option[File], extraEnv: (String, String)*): ProcessBuilder = {
    val jpb = new JProcessBuilder(command.toArray: _*)
    cwd.foreach(jpb directory _)
    extraEnv.foreach { case (k, v) => jpb.environment.put(k, v) }
    apply(jpb)
  }
  def apply(builder: JProcessBuilder): ProcessBuilder = new SimpleProcessBuilder(builder)
  def apply(file: File): FilePartialBuilder = new FileBuilder(file)
  def apply(url: URL): URLPartialBuilder = new URLBuilder(url)

  def applySeq[T](builders: Seq[T])(implicit convert: T => SourcePartialBuilder): Seq[SourcePartialBuilder] = builders.map(convert)

  def apply(value: Boolean): ProcessBuilder = apply(value.toString, if (value) 0 else 1)
  def apply(name: String, exitValue: => Int): ProcessBuilder = new DummyProcessBuilder(name, exitValue)

  def cat(file: SourcePartialBuilder, files: SourcePartialBuilder*): ProcessBuilder = cat(file :: files.toList)
  def cat(files: Seq[SourcePartialBuilder]): ProcessBuilder =
    {
      require(files.nonEmpty)
      files.map(_.cat).reduceLeft(_ #&& _)
    }
}

trait SourcePartialBuilder extends NotNull {
  /** Writes the output stream of this process to the given file. */
  def #>(f: File): ProcessBuilder = toFile(f, false)
  /** Appends the output stream of this process to the given file. */
  def #>>(f: File): ProcessBuilder = toFile(f, true)
  /**
   * Writes the output stream of this process to the given OutputStream. The
   * argument is call-by-name, so the stream is recreated, written, and closed each
   * time this process is executed.
   */
  def #>(out: => OutputStream): ProcessBuilder = #>(new OutputStreamBuilder(out))
  def #>(b: ProcessBuilder): ProcessBuilder = new PipedProcessBuilder(toSource, b, false, ExitCodes.firstIfNonzero)
  private def toFile(f: File, append: Boolean) = #>(new FileOutput(f, append))
  def cat = toSource
  protected def toSource: ProcessBuilder
}
trait SinkPartialBuilder extends NotNull {
  /** Reads the given file into the input stream of this process. */
  def #<(f: File): ProcessBuilder = #<(new FileInput(f))
  /** Reads the given URL into the input stream of this process. */
  def #<(f: URL): ProcessBuilder = #<(new URLInput(f))
  /**
   * Reads the given InputStream into the input stream of this process. The
   * argument is call-by-name, so the stream is recreated, read, and closed each
   * time this process is executed.
   */
  def #<(in: => InputStream): ProcessBuilder = #<(new InputStreamBuilder(in))
  def #<(b: ProcessBuilder): ProcessBuilder = new PipedProcessBuilder(b, toSink, false, ExitCodes.firstIfNonzero)
  protected def toSink: ProcessBuilder
}

trait URLPartialBuilder extends SourcePartialBuilder
trait FilePartialBuilder extends SinkPartialBuilder with SourcePartialBuilder {
  def #<<(f: File): ProcessBuilder
  def #<<(u: URL): ProcessBuilder
  def #<<(i: => InputStream): ProcessBuilder
  def #<<(p: ProcessBuilder): ProcessBuilder
}

/**
 * Represents a process that is running or has finished running.
 * It may be a compound process with several underlying native processes (such as 'a #&& b`).
 */
trait Process extends NotNull {
  /** Blocks until this process exits and returns the exit code.*/
  def exitValue(): Int
  /** Destroys this process. */
  def destroy(): Unit
}
/** Represents a runnable process. */
trait ProcessBuilder extends SourcePartialBuilder with SinkPartialBuilder {
  /**
   * Starts the process represented by this builder, blocks until it exits, and returns the output as a String.  Standard error is
   * sent to the console.  If the exit code is non-zero, an exception is thrown.
   */
  def !! : String
  /**
   * Starts the process represented by this builder, blocks until it exits, and returns the output as a String.  Standard error is
   * sent to the provided ProcessLogger.  If the exit code is non-zero, an exception is thrown.
   */
  def !!(log: ProcessLogger): String
  /**
   * Starts the process represented by this builder.  The output is returned as a Stream that blocks when lines are not available
   * but the process has not completed.  Standard error is sent to the console.  If the process exits with a non-zero value,
   * the Stream will provide all lines up to termination and then throw an exception.
   */
  def lines: Stream[String]
  /**
   * Starts the process represented by this builder.  The output is returned as a Stream that blocks when lines are not available
   * but the process has not completed.  Standard error is sent to the provided ProcessLogger.  If the process exits with a non-zero value,
   * the Stream will provide all lines up to termination but will not throw an exception.
   */
  def lines(log: ProcessLogger): Stream[String]
  /**
   * Starts the process represented by this builder.  The output is returned as a Stream that blocks when lines are not available
   * but the process has not completed.  Standard error is sent to the console. If the process exits with a non-zero value,
   * the Stream will provide all lines up to termination but will not throw an exception.
   */
  def lines_! : Stream[String]
  /**
   * Starts the process represented by this builder.  The output is returned as a Stream that blocks when lines are not available
   * but the process has not completed.  Standard error is sent to the provided ProcessLogger. If the process exits with a non-zero value,
   * the Stream will provide all lines up to termination but will not throw an exception.
   */
  def lines_!(log: ProcessLogger): Stream[String]
  /**
   * Starts the process represented by this builder, blocks until it exits, and returns the exit code.  Standard output and error are
   * sent to the console.
   */
  def ! : Int
  /**
   * Starts the process represented by this builder, blocks until it exits, and returns the exit code.  Standard output and error are
   * sent to the given ProcessLogger.
   */
  def !(log: ProcessLogger): Int
  /**
   * Starts the process represented by this builder, blocks until it exits, and returns the exit code.  Standard output and error are
   * sent to the console.  The newly started process reads from standard input of the current process.
   */
  def !< : Int
  /**
   * Starts the process represented by this builder, blocks until it exits, and returns the exit code.  Standard output and error are
   * sent to the given ProcessLogger.  The newly started process reads from standard input of the current process.
   */
  def !<(log: ProcessLogger): Int
  /** Starts the process represented by this builder.  Standard output and error are sent to the console.*/
  def run(): Process
  /** Starts the process represented by this builder.  Standard output and error are sent to the given ProcessLogger.*/
  def run(log: ProcessLogger): Process
  /** Starts the process represented by this builder.  I/O is handled by the given ProcessIO instance.*/
  def run(io: ProcessIO): Process
  /**
   * Starts the process represented by this builder.  Standard output and error are sent to the console.
   * The newly started process reads from standard input of the current process if `connectInput` is true.
   */
  def run(connectInput: Boolean): Process
  /**
   * Starts the process represented by this builder, blocks until it exits, and returns the exit code.  Standard output and error are
   * sent to the given ProcessLogger.
   * The newly started process reads from standard input of the current process if `connectInput` is true.
   */
  def run(log: ProcessLogger, connectInput: Boolean): Process

  def runBuffered(log: ProcessLogger, connectInput: Boolean): Process

  /** Constructs a command that runs this command first and then `other` if this command succeeds.*/
  def #&&(other: ProcessBuilder): ProcessBuilder
  /** Constructs a command that runs this command first and then `other` if this command does not succeed.*/
  def #||(other: ProcessBuilder): ProcessBuilder
  /**
   * Constructs a command that will run this command and pipes the output to `other`.
   * `other` must be a simple command.
   * The exit code will be that of `other` regardless of whether this command succeeds.
   */
  def #|(other: ProcessBuilder): ProcessBuilder
  /** Constructs a command that will run this command and then `other`.  The exit code will be the exit code of `other`.*/
  def ###(other: ProcessBuilder): ProcessBuilder

  def canPipeTo: Boolean
}
/** Each method will be called in a separate thread.*/
final class ProcessIO(val writeInput: OutputStream => Unit, val processOutput: InputStream => Unit, val processError: InputStream => Unit, val inheritInput: JProcessBuilder => Boolean) extends NotNull {
  def withOutput(process: InputStream => Unit): ProcessIO = new ProcessIO(writeInput, process, processError, inheritInput)
  def withError(process: InputStream => Unit): ProcessIO = new ProcessIO(writeInput, processOutput, process, inheritInput)
  def withInput(write: OutputStream => Unit): ProcessIO = new ProcessIO(write, processOutput, processError, inheritInput)
}
trait ProcessLogger {
  def info(s: => String): Unit
  def error(s: => String): Unit
  def buffer[T](f: => T): T
}
